package com.txtsqlclient.dataaccess.database.connection;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.txtsqlclient.dataaccess.database.QueryExecutionDAO;
import com.txtsqlclient.domain.DataSource;
import com.txtsqlclient.ui.vo.Session;

/**
 * This class is a factory, to obtain Database connections. This can be later
 * modified to implement connection pooling etc. This needs to be accessed using
 * singleton pattern.
 * 
 * @author spadda
 */
public class ConnectionFactory {

	private static ConnectionFactory factory = null;

	private final Map<DataSource, Connection> connections;

	private static Log LOGGER = LogFactory.getLog(QueryExecutionDAO.class);

	protected ConnectionFactory() {
		this.connections = new HashMap<DataSource, Connection>();
	}

	/**
	 * This method is required to get an instance of the connection factory.
	 * 
	 * @return the singleton instance of the factory.
	 */
	public static ConnectionFactory getInstance() {
		if (factory == null) {
			LOGGER
					.debug("Factory instance does not exist. Creating a new instance.");
			factory = new ConnectionFactory();
		}
		return factory;
	}

	/**
	 * This method is required to get an already opened database connection.
	 * 
	 * @param connectionName
	 * @return Database connection.
	 * @throws ConnectionException
	 */
	public Connection getConnection(final Session session)
			throws ConnectionException {
		// TODO Re-think if we need this when the session can support multiple
		// Data Sources.
		final DataSource dataSource = session.getDataSource();
		return getConnection(dataSource);
	}

	/**
	 * This method is required to get a database connection.
	 * 
	 * @param dataSource
	 * @return
	 * @throws ConnectionException
	 */
	public Connection getConnection(final DataSource dataSource)
			throws ConnectionException {
		Connection dbCon = this.connections.get(dataSource);
		try {
			if (dbCon == null || dbCon.isClosed()) {
				LOGGER
						.info("Opening a new connection as a one does not exit or is already closed for data source: "
								+ dataSource);
				dbCon = dataSource.openConnection();
				this.connections.put(dataSource, dbCon);
			}
		} catch (SQLException sqle) {
			throw new ConnectionException(
					"Error while retrieving the pooled connection", sqle);
		} catch (ConnectionException ce) {
			throw new ConnectionException("Error while reopening connection",
					ce);
		}
		LOGGER.debug("Successfully retrieved connection for data source: "
				+ dataSource);
		return dbCon;
	}

	/**
	 * This method is required to close the database connection.
	 * 
	 * @param session -
	 *            the session whose connection should be closed.
	 * @throws ConnectionException
	 */
	public void closeConnection(final Session session)
			throws ConnectionException {
		// TODO Modify this to close either a)active connection or b)all
		// connections.
		final DataSource ds = session.getDataSource();
		closeConnection(ds);
	}

	/**
	 * This method is required to close the database connection of a data
	 * source.
	 * 
	 * @param dataSource
	 *            The Data Source whose connection should be closed.
	 * @throws ConnectionException
	 */
	public void closeConnection(final DataSource dataSource)
			throws ConnectionException {
		Connection dbCon = null;
		dbCon = this.connections.get(dataSource);
		if (dbCon == null) {
			LOGGER.info("No connection exists for the given data source: "
					+ dataSource);
			throw new ConnectionException(
					"No connection exists for this data source:" + dataSource);
		}
		try {
			if (dbCon.isClosed()) {
				LOGGER
						.info("Connection exists but is closed for the given data source: "
								+ dataSource);
				this.connections.remove(dataSource);
				return;
			}
			dbCon.close();
			this.connections.remove(dataSource);
		} catch (SQLException sqle) {
			throw new ConnectionException(
					"Error while closing database connection", sqle);
		}
		LOGGER.debug("Successfully closed connection for the data source: "
				+ dataSource);
	}

	/**
	 * This method is required to clean up the factory and close all open DB
	 * connections.
	 */
	public void cleanUp() throws ConnectionException {
		final List<DataSource> failedProfiles = new ArrayList<DataSource>();
		final List<DataSource> removedProfiles = new ArrayList<DataSource>();
		for (DataSource dataSource : this.connections.keySet()) {
			try {
				this.closeConnection(dataSource);
				removedProfiles.add(dataSource);
			} catch (ConnectionException ce) {
				failedProfiles.add(dataSource);
			}
		}
		if (!failedProfiles.isEmpty()) {
			throw new ConnectionException(
					"Failed closing connections for some profiles: "
							+ failedProfiles);
		}
		LOGGER.debug("Successfully cleaned up all connections");
	}
}